package autoCode

import (
	"errors"
	"github.com/Sir-yuChen/funAnalyse/global"
	"github.com/Sir-yuChen/funAnalyse/model"
	"github.com/Sir-yuChen/funAnalyse/service/autoCode/modeltool"
	"github.com/Sir-yuChen/funAnalyse/utils"
	"go.uber.org/zap"
	"os"
	"path"
	"strings"
	"text/template"
)

//生成模板代码核心方法

const (
	tplFilesPath = "resource/autocode_template"
	resourcePath = "resource/autocode" //生成代码的默认文件
)

type AutoCodeService struct{}

func NewAutoCodeService() *AutoCodeService {
	return &AutoCodeService{}
}

// CreateFileByTpl 代码生成器  目前仅仅支持生成一次，再次生成将会覆盖已存在文件
func (receiver *AutoCodeService) CreateFileByTpl(autoCodeList []*utils.AutoCodeStruct) (e error) {
	autoCodeList, e = receiver.InitAutoCodePattern(autoCodeList)
	if e != nil {
		return e
	}
	if autoCodeList == nil {
		global.GVA_LOG.Warn("代码生成器》》》》初始化模式下无可生成的代码文件")
		return nil
	}
	//正常流程
	for _, codeStruct := range autoCodeList {
		tempList, e := receiver.ReadyTemp(codeStruct)
		if e != nil {
			global.GVA_LOG.Error("代码生成器》》》》4.准备template 操作模板异常")
			return e
		}
		e = receiver.ReadyData(codeStruct)
		if e != nil {
			global.GVA_LOG.Error("代码生成器》》》》模板数据准备异常")
			return e
		}
		//创建文件夹
		for _, data := range tempList {
			codePath := data.AutoCodePath
			lastInd := strings.LastIndex(codePath, "/")
			if lastInd == -1 {
				//说明字符串不存在
				lastInd = strings.LastIndex(codePath, "\\")
			}
			pathStr := codePath[:lastInd]
			//判断文件夹是否存在
			exist, e := utils.FileExist(pathStr)
			if e != nil {
				global.GVA_LOG.Error("代码生成器》》》》校验生成代码后文件路径异常", zap.Error(e))
				return errors.New("校验生成代码后文件路径异常")
			}
			if !exist {
				//不存在创建文件文件夹
				if e := modeltool.CreateDir(pathStr); e != nil {
					global.GVA_LOG.Error("代码生成器》》》》create directory ERROR", zap.Error(e))
					return e
				}
			}
			//调用模板生成方法开始生成代码
			if e = receiver.ExecuteTpl(&data, codeStruct); e != nil {
				global.GVA_LOG.Error("代码生成器》》》》模板生成失败", zap.Error(e))
				return e
			}
		}
	}
	global.GVA_LOG.Info("代码生成器》》》》代码生成成功！！！")
	return nil
}

// InitAutoCodePattern 初始化模式下 需要生成所有表的模板文件
func (receiver *AutoCodeService) InitAutoCodePattern(auto []*utils.AutoCodeStruct) (autoCodeList []*utils.AutoCodeStruct, err error) {
	newAuto := []*utils.AutoCodeStruct{}
	//1. 判断是否是初始化模式
	for _, v := range auto {
		if v.AutoCodePattern != "-1" {
			return auto, nil
		}
		//初始化模式查询出所有的表名
		dbcolumns := utils.NewDBColumns()
		tables, err := dbcolumns.GetTables(global.GVA_CONFIG.Mysql.Dbname)
		if err != nil {
			global.GVA_LOG.Error("代码生成器》》》》获取数据库下所有表名异常", zap.String("dbName", global.GVA_CONFIG.Mysql.Dbname))
			return nil, err
		}
		for _, t := range tables {
			var n = utils.AutoCodeStruct{
				AutoCodePattern: v.AutoCodePattern,
				PkgName:         v.PkgName,
				TableName:       t.TableName,
				HasTimer:        false,
				Fields:          nil,
			}
			newAuto = append(newAuto, &n)
		}
	}
	return newAuto, nil
}

// ExecuteTpl 生成方法
func (receiver *AutoCodeService) ExecuteTpl(tpl *utils.TplData, autoCode *utils.AutoCodeStruct) (e error) {
	//读取模板文件内容
	f, err := os.OpenFile(tpl.AutoCodePath, os.O_CREATE|os.O_WRONLY, 0o755)
	if err != nil {
		return err
	}
	tplList := []string{tpl.PresetTplPath}
	//1. 模板函数 解析模板之前添加 函数 template.New() 函数中参数名字要和 template.ParseFiles() 函数的文件名要相同
	//must 加载失败时 panic
	//注意：template.New 必须为当前模板的文件名
	render := template.Must(template.New(tpl.PresetTplName).
		Funcs(template.FuncMap{
			"FirstCharUpper":     utils.FirstCharUpper,  //格式话参数
			"FirstCharLowerCase": utils.FirstCharLower,  //格式化参数
			"TypeConvert":        modeltool.TypeConvert, //参数类型转换 DB-->Go
		}).ParseFiles(tplList...))
	//生产并输出到指定文件
	//注意 ExecuteTemplate 第二个参数必须为当前模板的文件名 与new 保持一致
	e = render.ExecuteTemplate(f, tpl.PresetTplName, autoCode)
	f.Close() //此处必须提前关闭，因为差异化对比完要删除文件，不能占用通道
	/*
		1、使用 New() 在创建时就为其添加一个模板名称，并且执行 t.Execute() 会默认去寻找该名称进行数据融合；
		2、使用 ParseFiles() 创建模板可以一次指定多个文件加载多个模板进来，但是就不可以使用 t.Execute() 来执行数据融合；
		因为使用 t.Execute() 无法找到要使用哪个加载过的模板进行数据融合，而只有New()创建时才会指定一个 t.Execute() 执行时默认加载的模板。
		但是 ParseFiles() 可以通过
		func (t *Template) ExecuteTemplate(wr io.Writer, name string, data interface{}) error
		来进行数据融合，因为该函数可以指定模板名，因此，实例模板就可以知道要去加载自己内部的哪一个模板进行数据融合。
		当然无论使用 New() 还是 ParseFiles() 创建模板，都是可以使用 ExecuteTemplate() 来进行数据融合，
		但是对于 Execute() 一般与 New() 创建的模板进行配合使用。
	*/
	//校验是否是临时文件
	fileName := path.Base(tpl.AutoCodePath)
	if strings.Contains(fileName, "_temp") {
		//path := modeltool.HandleTempPath(tpl.AutoCodePath)
		////TODO 文件差异化对比 bug巨多，暂时无法实现
		//e = modeltool.FileCompareAndMerge(path, tpl.AutoCodePath)
		//if e != nil {
		//	global.GVA_LOG.Error("代码生成器》》》》新旧文件比较并合并异常", zap.Any("tpl", tpl), zap.Error(e))
		//	return e
		//}
		global.GVA_LOG.Warn("文件差异化对比 bug巨多,暂时无法实现")
	}
	return e
}

// ReadyData 准备待填充的数据
func (receiver *AutoCodeService) ReadyData(autoCode *utils.AutoCodeStruct) (e error) {
	dbcolumns := utils.NewDBColumns()
	dbcolumns.TableName = autoCode.TableName
	columns, e := dbcolumns.GetDBColumns()
	if e != nil {
		global.GVA_LOG.Error("代码生成器》》》》5.获取待填充数据异常")
		return e
	}
	//封装参数
	autoCode.Fields = columns
	//确认是否有时间类型的参数
	for _, v := range columns {
		if v.DataType == "date" || v.DataType == "timestamp" || v.DataType == "datetime" {
			autoCode.HasTimer = true
			break
		}
	}
	return nil
}

// ReadyTemp 准备template 操作模板
func (receiver *AutoCodeService) ReadyTemp(autoCode *utils.AutoCodeStruct) (tempList []utils.TplData, err error) {
	//去除所有空格
	modeltool.TrimSpace(autoCode)
	for _, field := range autoCode.Fields {
		modeltool.TrimSpace(field)
	}
	//生成代码的文件名
	if autoCode.AutoCodeName == "" {
		autoCode.AutoCodeName = utils.FirstCharLower(autoCode.TableName)
	}
	//拼接路由 项目名+包名 todo 路由不正确待优化
	lower := utils.FirstCharLower(autoCode.PkgName)
	autoCode.RouterGroup = global.GVA_CONFIG.System.ApplicationName + "/" + lower + "/" + autoCode.AutoCodeName
	//根据tpl模板生成 TplData 结构体  等待下一步填充数据
	codeTpl, err := receiver.HandleTplAndCodeFile(autoCode.AutoCodePattern, autoCode.PkgName, autoCode.AutoCodeName)
	if err != nil {
		global.GVA_LOG.Error("代码生成器》》》》获取特定模板信息异常")
		return nil, err
	}
	tempList = make([]utils.TplData, 0, len(codeTpl))
	for _, v := range codeTpl {
		tempList = append(
			tempList,
			utils.TplData{
				PresetTplPath: v.TplPath,
				PresetTplName: v.TplName,
				AutoPackage:   autoCode.PkgName,
				AutoCodePath:  v.TplAutoPath,
			},
		)
	}
	return tempList, nil
}

// HandleTplAndCodeFile 将预设模板与代码实际生成文件一一对应
func (receiver *AutoCodeService) HandleTplAndCodeFile(autoCodePattern string, pkgName string, autoCodeName string) (tplList []*model.StockCodeTpl, err error) {
	var codeTplList []*model.StockCodeTpl
	if autoCodePattern == "-1" {
		err = global.GVA_DB.Model(model.StockCodeTpl{}).Where("status = ? AND tpl_type in( ? ) ", "Y", global.GVA_CONFIG.Tpl.BasicPattern).Find(&codeTplList).Error
	} else {
		err = global.GVA_DB.Model(model.StockCodeTpl{}).Where(" status = ? AND tpl_type = ? ", "Y", autoCodePattern).Find(&codeTplList).Error
	}
	if err != nil {
		global.GVA_LOG.Error("代码生成器》》》》3.获取特定类型DB模板信息异常", zap.Error(err), zap.Any("AutoCodePattern", autoCodePattern))
		return nil, err
	}
	if codeTplList == nil || len(codeTplList) == 0 {
		global.GVA_LOG.Warn("代码生成器》》》》无可使用的模板", zap.Error(err), zap.Any("AutoCodePattern", autoCodePattern))
		return nil, err
	}
	absPath := utils.GetAbsPath()
	for _, tpl := range codeTplList {
		//校验模板文件是否存在
		exist, err := utils.FileExist(tpl.TplPath)
		if err != nil {
			global.GVA_LOG.Error("代码生成器》》》》校验模板文件是否存在异常", zap.Error(err), zap.Any("tpl", tpl))
			return nil, err
		}
		if !exist {
			global.GVA_LOG.Error("代码生成器》》》》模板文件不存在,请检查模板文件", zap.Error(err), zap.Any("tpl", tpl))
			return nil, nil
		}
		//确认生成代码的模板以及生成代码文件路径
		tplName := path.Base(tpl.TplPath)                       //文件名带后缀
		fileSuffix := path.Ext(tpl.TplPath)                     //文件后缀
		filenameOnly := strings.TrimSuffix(tplName, fileSuffix) //单独文件名
		autoCodeFileSuffix := path.Ext(filenameOnly)
		autoCodeFileameOnly := strings.TrimSuffix(filenameOnly, autoCodeFileSuffix) //单独文件名
		upper := utils.FirstCharUpper(tpl.TplAutoPath)
		autoCodeUpperName := utils.FirstCharUpper(filenameOnly)
		autoCodeFileameUpper := utils.FirstCharUpper(autoCodeFileameOnly)
		//对路由文件进行单独处理，每个pkg下只能有一个路由文件
		if strings.Contains(tplName, "enter") {
			tplAutoPath := path.Join(absPath, tpl.TplAutoPath, pkgName+upper, pkgName+autoCodeUpperName)
			//校验路由文件是否已经存在 存在则生成临时文件 将两个文件做对比 整合成新文件
			fileExist, err := utils.FileExist(tplAutoPath)
			if err != nil {
				global.GVA_LOG.Error("代码生成器》》》》校验包下路由文件是否已存在异常", zap.Error(err), zap.Any("tpl", tpl))
				return nil, nil
			}
			if fileExist {
				tpl.TplAutoPath = path.Join(absPath, tpl.TplAutoPath, pkgName+upper, pkgName+autoCodeFileameUpper+"_temp"+autoCodeFileSuffix)
				global.GVA_LOG.Info("代码生成器》》》》包下路由文件已存在,生成临时路由文件", zap.Any("TplAutoPath", tpl.TplAutoPath))
			} else {
				tpl.TplAutoPath = tplAutoPath
			}
		} else {
			tpl.TplAutoPath = path.Join(absPath, tpl.TplAutoPath, pkgName+upper, autoCodeName+autoCodeFileSuffix)
		}
		tpl.TplName = tplName
	}
	return codeTplList, err
}
